[Apollo Client v3] fetchMore を使ったページネーションの実装例
実現したいこと
初期ロードで 10 件のデータを画面に表示し、もっと見るボタンを押すと続きの 10 件を取得します。 このもっと見るボタンを押した時に取得した追加のデータを読み込み済みのデータにマージするために Apollo Client の typePolicies を設定します。
検証環境
- React "18.2.0"
- Apollo Client "3.6.9"
- GraphQL "16.5.0"
スキーマからのクエリや型の生成には codegen を利用しています。
利用する GraphQL API
サーバーサイド API のスキーマです。 first の値に取得したいデータ数、after にページネーションの次に取得するデータの ID を渡す作りになっています。
edges の中に画面に一覧表示したいデータが配列で入ります。
type SampleEdgeNode implements BaseSample { id: ID! totalQuantity: Int! shopName: String! issuedDate: String! issuedAt: String! } type SampleEdge { cursor: String! node: SampleEdgeNode! } type SamplesConnection { pageInfo: PageInfo! edges: [SampleEdge!] } type Query { samples(first: Int, after: String): SamplesConnection! }
今回取得したいデータはこのようにネストされたデータ形式になります。
{ "data": { "samples": { "pageInfo": { "hasPreviousPage": false, "startCursor": "eyJpZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMy0xMCIsImxpbmVVc2VySWQiOiJVYTNlZmU4OWFhOGY5ZDlkMmU5ZDE0NDZhN2QyNDk2MzMiLCJpc3N1ZWRBdCI6IjIwMjItMDQtMDFUMjA6MDA6MDArMDk6MDAifQ==", "hasNextPage": true, "endCursor": "eyJpZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMy04IiwibGluZVVzZXJJZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMyIsImlzc3VlZEF0IjoiMjAyMi0wNC0wMVQxODowMDowMCswOTowMCJ9", "__typename": "PageInfo" }, "edges": [ { "cursor": "eyJpZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMy0xMCIsImxpbmVVc2VySWQiOiJVYTNlZmU4OWFhOGY5ZDlkMmU5ZDE0NDZhN2QyNDk2MzMiLCJpc3N1ZWRBdCI6IjIwMjItMDQtMDFUMjA6MDA6MDArMDk6MDAifQ==", "node": { "id": "Ua3efe89aa8f9d9d2e9d1446a7d249633-10", "totalQuantity": 2, "shopName": "A店", "issuedAt": "2022-04-01T20:00:00+09:00", "issuedDate": "2022-04-01", "__typename": "SampleEdgeNode" }, "__typename": "SampleEdge" }, { "cursor": "eyJpZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMy05IiwibGluZVVzZXJJZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMyIsImlzc3VlZEF0IjoiMjAyMi0wNC0wMVQxOTowMDowMCswOTowMCJ9", "node": { "id": "Ua3efe89aa8f9d9d2e9d1446a7d249633-9", "totalQuantity": 2, "shopName": "A店", "issuedAt": "2022-04-01T19:00:00+09:00", "issuedDate": "2022-04-01", "__typename": "SampleEdgeNode" }, "__typename": "SampleEdge" }, { "cursor": "eyJpZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMy04IiwibGluZVVzZXJJZCI6IlVhM2VmZTg5YWE4ZjlkOWQyZTlkMTQ0NmE3ZDI0OTYzMyIsImlzc3VlZEF0IjoiMjAyMi0wNC0wMVQxODowMDowMCswOTowMCJ9", "node": { "id": "Ua3efe89aa8f9d9d2e9d1446a7d249633-8", "totalQuantity": 2, "shopName": "A店", "issuedAt": "2022-04-01T18:00:00+09:00", "issuedDate": "2022-04-01", "__typename": "SampleEdgeNode" }, "__typename": "SampleEdge" } ], "__typename": "SampleConnection" } } }
useQueryでデータを取得
上記の API からクエリでデータを取得します。
query samples($first: Int, $after: String) { samples(first: $first, after: $after) { pageInfo { hasPreviousPage startCursor hasNextPage endCursor } edges { cursor node { id totalQuantity shopName issuedAt issuedDate } } } }
コンポーネントから useQuery を使ってデータを取得します。
const {loading, data} = useQuery<SamplesQuery>(SamplesDocument, { variables: { first: 3, // 一度のコールで取得するデータの数 cursor: null, // 初回読み込み時はnullを渡す }, });
スキーマからの型の生成には codegen を利用しています。 以下が自動生成された内容になります。
export const SamplesDocument = gql` query samples($first: Int, $after: String) { samples(first: $first, after: $after) { pageInfo { hasPreviousPage startCursor hasNextPage endCursor } edges { cursor node { id totalQuantity shopName issuedAt issuedDate } } } } `; export type SamplesQuery = { __typename?: "Query"; sampless: { __typename?: "SampleConnection"; pageInfo: { __typename?: "PageInfo"; hasPreviousPage: boolean; startCursor?: string | null; hasNextPage: boolean; endCursor?: string | null; }; edges?: Array<{ __typename?: "SampleEdge"; cursor: string; node: { __typename?: "SampleEdgeNode"; id: string; totalQuantity: number; shopName: string; issuedAt: string; issuedDate: string; }; }> | null; }; };
これで初期ロード時に 10 件のデータを取得できるようになりました。
fetchMore 関数で次の 10 件を取得
次にコンポーネントにもっと見るボタンと追加のデータを取得する関数を追加します。
samples.tsx
import {useQuery} from "@apollo/client"; import {FC, useCallback} from "react"; import {SamplesDocument, SamplesQuery} from "../../../generated/graphql"; import {LoadingPanel} from "../../atoms/LoadingPanel/LoadingPanel"; import {SystemError} from "../SystemError"; import {NotFoundSamples} from "./NotFoundSamples"; import {SampleCards} from "./SampleCards"; export const Samples: FC = () => { const {loading, data, fetchMore} = useQuery<SamplesQuery>(SamplesDocument, { variables: { first: 3, // 一度のコールで取得するデータの数 cursor: null, // 初回ロード時はnullを渡す }, }); const loadMore = useCallback(async () => { // 次の10件を取得 await fetchMore({ variables: { cursor: data?.samples.pageInfo.endCursor, }, }); }, [data, fetchMore]); if (loading) { return <LoadingPanel />; } if (data === undefined) { return <SystemError />; } return ( <div> <div className="container bg-grayLight mx-auto h-screen"> <div className="px-6" data-test="samplesButton"> {/* データが1件以上なら一覧ページを表示し、0件の場合はNotFoundコンポーネントを表示する */} {data.samples.edges != null && data?.samples.edges?.length > 0 ? ( <SampleCards data={data} loadMore={loadMore} /> ) : ( <NotFoundSamples /> )} </div> </div> </div> ); };
この実装では loadMoreが呼ばれたときに次のデータ 10 件を取得します。 何も設定しないままだと、初回ロードの時に読み込んだデータを上書きする形で Apollo のキャッシュが上書きされます。
今回は初回ロードの時に取得したデータ 10 件に次の 10 件をプラスして表示させたいため、Apollo の typePolicies の設定でデータをマージします。
typePolicies で fetchMore で取得したデータを取得済みのデータとマージする
取得済みのデータと fetchMore で新しく取得したデータをマージする処理を typePolicies に記述します。
import {ApolloClient, InMemoryCache} from "@apollo/client"; import {SamplesQuery} from "./generated/graphql"; const endpoint = (import.meta.env.VITE_API_ENDPOINT as string) ?? ""; type SamplesData = SamplesQuery["samples"]; export const client = (liffIdToken: string | null) => { const apolloClient = new ApolloClient({ uri: new URL("/graphql", endpoint).toString(), cache: new InMemoryCache({ typePolicies: { Query: { fields: { // フィールド名 samples: { keyArgs: false, // fetchMoreで取得したデータと取得済みのデータをマージ // https://www.apollographql.com/docs/react/pagination/core-api/ merge(existing: SamplesData, incoming: SamplesData) { return { ...(incoming ?? {}), edges: [ ...(existing?.edges ?? []), ...(incoming?.edges ?? []), ], }; }, }, }, }, }, }), headers: { Authorization: IdToken ?? "", }, }); return apolloClient; };
今回のようにデータの形式がネストされている場合は以下のようにマージしたいデータを指定する必要があります。 以下の例では edges に配列で渡されるデータを取得済みのデータに追加してキャッシュするように指定しています。